﻿using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.Reflection;
using System.Runtime.CompilerServices;

namespace EasySM
{
    public enum StateTranstion
    {
        Safe,
        Overwrite,
    }

    public interface IStateMachine
    {
        MonoBehaviour Component { get; }
        StateMapping CurrentStateMap { get; }
        bool IsInTransition { get; }
    }

    public class StateMapping
    {
        public System.Object State;

        public bool HasEnterRoutine;
        public Action EnterCall = StateMachineRunner.DoNothing;
        public Func<IEnumerator> EnterRoutine = StateMachineRunner.DoNothingCoroutine;

        public Action<System.Object> EnterWithParamCall = StateMachineRunner.DoNothingWithParam;

        public bool HasExitRoutine;
        public Action ExitCall = StateMachineRunner.DoNothing;
        public Func<IEnumerator> ExitRoutine = StateMachineRunner.DoNothingCoroutine;

        public Action Finally = StateMachineRunner.DoNothing;
        public Action Update = StateMachineRunner.DoNothing;
        public Action LateUpdate = StateMachineRunner.DoNothing;
        public Action FixedUpdate = StateMachineRunner.DoNothing;
        public Action<Collision> OnCollisionEnter = StateMachineRunner.DoNothingCollision;
        public Action<Collision> OnCollisionExit = StateMachineRunner.DoNothingCollision;
        public Action<Collider> OnTriggerEnter = StateMachineRunner.DoNothingCollider;
        public Action<Collider> OnTriggerExit = StateMachineRunner.DoNothingCollider;

        public StateMapping(System.Object state)
        {
            State = state;
        }
    }

    public class StateMachineRunner : MonoBehaviour
    {
        private List<IStateMachine> mStateMachineList = new List<IStateMachine>();

        public EasyStateMachine<T> Initialize<T>(MonoBehaviour component) where T : struct, IConvertible, IComparable
        {
            var fsm = new EasyStateMachine<T>(this, component);
            mStateMachineList.Add(fsm);
            return fsm;
        }

        public EasyStateMachine<T> Initialize<T>(MonoBehaviour component, T startState) where T : struct, IConvertible, IComparable
        {
            var fsm = Initialize<T>(component);
            fsm.ChangeState(startState);
            return fsm;
        }

        private void FixedUpdate()
        {
            for (int i = 0; i < mStateMachineList.Count; i++)
            {
                var fsm = mStateMachineList[i];
                if (!fsm.IsInTransition && fsm.Component.enabled)
                {
                    fsm.CurrentStateMap.FixedUpdate();
                }
            }
        }

        private void Update()
        {
            for (int i = 0; i < mStateMachineList.Count; i++)
            {
                var fsm = mStateMachineList[i];
                if (!fsm.IsInTransition && fsm.Component.enabled)
                {
                    fsm.CurrentStateMap.Update();
                }
            }
        }

        private void LateUpdate()
        {
            for (int i = 0; i < mStateMachineList.Count; i++)
            {
                var fsm = mStateMachineList[i];
                if (!fsm.IsInTransition && fsm.Component.enabled)
                {
                    fsm.CurrentStateMap.LateUpdate();
                }
            }
        }

        private void OnCollisionEnter(Collision collision)
        {
            for (int i = 0; i < mStateMachineList.Count; i++)
            {
                var fsm = mStateMachineList[i];
                if (!fsm.IsInTransition && fsm.Component.enabled)
                {
                    fsm.CurrentStateMap.OnCollisionEnter(collision);
                }
            }
        }

        private void OnCollisionExit(Collision collision)
        {
            for (int i = 0; i < mStateMachineList.Count; i++)
            {
                var fsm = mStateMachineList[i];
                if (!fsm.IsInTransition && fsm.Component.enabled)
                {
                    fsm.CurrentStateMap.OnCollisionExit(collision);
                }
            }
        }

        private void OnTriggerEnter(Collider other)
        {
            for (int i = 0; i < mStateMachineList.Count; i++)
            {
                var fsm = mStateMachineList[i];
                if (!fsm.IsInTransition && fsm.Component.enabled)
                {
                    fsm.CurrentStateMap.OnTriggerEnter(other);
                }
            }
        }

        private void OnTriggerExit(Collider other)
        {
            for (int i = 0; i < mStateMachineList.Count; i++)
            {
                var fsm = mStateMachineList[i];
                if (!fsm.IsInTransition && fsm.Component.enabled)
                {
                    fsm.CurrentStateMap.OnTriggerExit(other);
                }
            }
        }

        public static void DoNothing() { }
        public static void DoNothingWithParam(System.Object param) { }
        public static void DoNothingCollider(Collider collider) { }
        public static void DoNothingCollision(Collision other) { }
        public static IEnumerator DoNothingCoroutine() { yield break; }
    }

    public class EasyStateMachine<T> : IStateMachine where T : struct, IConvertible, IComparable
    {
        public event Action<T> Changed;

        private StateMachineRunner mEngine;
        private MonoBehaviour mComponent;

        private StateMapping mLastState;
        private StateMapping mCurrentState;
        private StateMapping mDestinationState;

        public T LastState { get { if (mLastState == null) return default(T); return (T)mLastState.State; } }
        public T State { get { if (mCurrentState == null) return default(T); else return (T)mCurrentState.State; } }
        public bool IsInTransition { get { return mIsInTransition; } }
        public StateMapping CurrentStateMap { get { return mCurrentState; } }
        public MonoBehaviour Component { get { return mComponent; } }

        private Dictionary<object, StateMapping> mStateLoopup;

        private bool mIsInTransition = false;

        private IEnumerator mCurrentTransition;
        private IEnumerator mExitRoutine;
        private IEnumerator mEnterRoutine;
        private IEnumerator mQueuedChange;

        public static EasyStateMachine<T> Initialize(MonoBehaviour component)
        {
            var engine = component.GetComponent<StateMachineRunner>();
            if (engine == null) engine = component.gameObject.AddComponent<StateMachineRunner>();
            return engine.Initialize<T>(component);
        }

        public static EasyStateMachine<T> Initialize(MonoBehaviour component, T startState)
        {
            var engine = component.GetComponent<StateMachineRunner>();
            if (engine == null) engine = component.gameObject.AddComponent<StateMachineRunner>();
            return engine.Initialize<T>(component, startState);
        }

        public EasyStateMachine(StateMachineRunner engine, MonoBehaviour component)
        {
            mEngine = engine;
            mComponent = component;

            // Define State
            var values = Enum.GetValues(typeof(T));
            if (values.Length < 1)
            {
                Debug.LogError("Enum provided to Initialize must have at least 1 visible definition!");
                return;
            }

            mStateLoopup = new Dictionary<object, StateMapping>();
            for (int i = 0; i < values.Length; i++)
            {
                var mapping = new StateMapping((Enum)values.GetValue(i));
                mStateLoopup.Add(mapping.State, mapping);
            }

            // Reflect methods
            var methods = component.GetType().GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.NonPublic);

            // Blind methods to states
            var separator = "_".ToCharArray();
            for (int i = 0; i < methods.Length; i++)
            {
                if (methods[i].GetCustomAttributes(typeof(CompilerGeneratedAttribute), true).Length != 0) continue;

                var names = methods[i].Name.Split(separator);

                // Ignore functions without an underLine
                if (names.Length <= 1) continue;

                Enum key;
                try { key = (Enum)Enum.Parse(typeof(T), names[0]); }
                catch { continue; }

                var targetState = mStateLoopup[key];

                switch (names[1])
                {
                    case "Enter":
                        if (methods[i].ReturnType == typeof(IEnumerator))
                        {
                            targetState.HasEnterRoutine = true;
                            targetState.EnterRoutine = CreateDelegate<Func<IEnumerator>>(methods[i], component);
                        }
                        else
                        {
                            targetState.HasEnterRoutine = false;
                            targetState.EnterCall = CreateDelegate<Action>(methods[i], component);
                        }
                        break;
                    case "Exit":
                        if (methods[i].ReturnType == typeof(IEnumerator))
                        {
                            targetState.HasExitRoutine = true;
                            targetState.ExitRoutine = CreateDelegate<Func<IEnumerator>>(methods[i], component);
                        }
                        else
                        {
                            targetState.HasExitRoutine = false;
                            targetState.ExitCall = CreateDelegate<Action>(methods[i], component);
                        }
                        break;
                    case "EnterParam": targetState.EnterWithParamCall = CreateDelegate<Action<System.Object>>(methods[i], component); break;
                    case "Finally": targetState.Finally = CreateDelegate<Action>(methods[i], component); break;
                    case "Update": targetState.Update = CreateDelegate<Action>(methods[i], component); break;
                    case "LateUpdate": targetState.LateUpdate = CreateDelegate<Action>(methods[i], component); break;
                    case "FixedUpdate": targetState.FixedUpdate = CreateDelegate<Action>(methods[i], component); break;
                    case "OnCollisionEnter": targetState.OnCollisionEnter = CreateDelegate<Action<Collision>>(methods[i], component); break;
                    case "OnCollisionExit": targetState.OnCollisionExit = CreateDelegate<Action<Collision>>(methods[i], component); break;
                    case "OnTriggerEnter": targetState.OnTriggerEnter = CreateDelegate<Action<Collider>>(methods[i], component); break;
                    case "OnTriggerExitr": targetState.OnTriggerExit = CreateDelegate<Action<Collider>>(methods[i], component); break;
                    default:
                        break;
                }
            }

            mCurrentState = new StateMapping(null);
        }

        private V CreateDelegate<V>(MethodInfo method, System.Object target) where V : class
        {
            var ret = Delegate.CreateDelegate(typeof(V), target, method) as V;
            if (ret == null) { Debug.LogError("Unable to create delegate for method called : " + method.Name); }
            return ret;
        }

        public void ChangeState(T newState, System.Object param = null)
        {
            ChangeState(newState, StateTranstion.Safe, param);
        }

        public void ChangeState(T newState, StateTranstion transition, System.Object param = null)
        {
            if (mStateLoopup == null)
            {
                Debug.LogError("States have not been configured, please call initialized beform trying to set state");
                return;
            }

            if (!mStateLoopup.ContainsKey(newState))
            {
                Debug.LogError("No state with the name " + newState.ToString() + " can be found. Please make sure you are called the correct type the statemachine was initialized with");
                return;
            }

            var nextState = mStateLoopup[newState];

            if (mCurrentState == nextState) return;

            // Cancel any queued changes
            if (mQueuedChange != null) { mEngine.StopCoroutine(mQueuedChange); mQueuedChange = null; }

            switch (transition)
            {
                case StateTranstion.Safe:
                    if (mIsInTransition)
                    {
                        if (mExitRoutine != null) { mDestinationState = nextState; return; }

                        if (mEnterRoutine != null)
                        {
                            mQueuedChange = WaitForPreviousTransition(nextState);
                            mEngine.StartCoroutine(mQueuedChange);
                            return;
                        }
                    }
                    break;
                case StateTranstion.Overwrite:
                    if (mCurrentState != null) mEngine.StopCoroutine(mCurrentTransition);
                    if (mExitRoutine != null) mEngine.StopCoroutine(mExitRoutine);
                    if (mEnterRoutine != null) mEngine.StopCoroutine(mEnterRoutine);
                    break;
                default:
                    break;
            }

            if ((mCurrentState != null && mCurrentState.HasExitRoutine) || nextState.HasEnterRoutine)
            {
                mIsInTransition = true;
                mCurrentTransition = ChangeToNewStateRoutine(nextState, transition);
                mEngine.StartCoroutine(mCurrentTransition);
            }
            // Same frame transition, no coroutines are present
            else
            {
                if (mCurrentState != null)
                {
                    mCurrentState.ExitCall();
                    mCurrentState.Finally();
                }

                mLastState = mCurrentState;
                mCurrentState = nextState;
                if (mCurrentState != null)
                {
                    mCurrentState.EnterCall();
                    mCurrentState.EnterWithParamCall(param);
                    if (Changed != null)
                    {
                        Changed((T)mCurrentState.State);
                    }
                }
                mIsInTransition = false;
            }
        }

        private IEnumerator WaitForPreviousTransition(StateMapping nextState)
        {
            while (mIsInTransition)
            {
                yield return null;
            }

            ChangeState((T)nextState.State);
        }

        private IEnumerator ChangeToNewStateRoutine(StateMapping newState, StateTranstion transition)
        {
            mDestinationState = newState;

            if (mCurrentState != null)
            {
                if (mCurrentState.HasExitRoutine)
                {
                    mExitRoutine = mCurrentState.ExitRoutine();

                    if (mExitRoutine != null && transition != StateTranstion.Overwrite)
                    {
                        yield return mEngine.StartCoroutine(mExitRoutine);
                    }
                    mExitRoutine = null;
                }
                else
                {
                    mCurrentState.ExitCall();
                }

                mCurrentState.Finally();
            }

            mLastState = mCurrentState;
            mCurrentState = mDestinationState;

            if (mCurrentState != null)
            {
                if (mCurrentState.HasEnterRoutine)
                {
                    mEnterRoutine = mCurrentState.EnterRoutine();

                    if (mEnterRoutine != null)
                    {
                        yield return mEngine.StartCoroutine(mEnterRoutine);
                    }
                    mEnterRoutine = null;
                }
                else
                {
                    mCurrentState.EnterCall();
                }

                if (Changed != null)
                {
                    Changed((T)mCurrentState.State);
                }
            }

            mIsInTransition = false;
        }


    }
}